/**
 * Copyright (C) 2010 Cloudfarming <info@cloudfarming.nl>
 *
 * Licensed under the Eclipse Public License - v 1.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.eclipse.org/legal/epl-v10.html
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package nl.cloudfarming.sensor.task;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import nl.cloudfarming.client.geoviewer.Layer;
import nl.cloudfarming.client.geoviewer.LayerProvider;
import nl.cloudfarming.client.sensor.SensorDataLayerProvider;
import nl.cloudfarming.client.sensor.SensorService;
import nl.cloudfarming.sensor.model.GeoSensorDataBO;
import nl.cloudfarming.sensor.model.GeoSensorDataFileBO;
import nl.cloudfarming.sensor.model.PlotBO;
import nl.cloudfarming.sensor.model.SensorRunBO;
import nl.cloudfarming.sensor.model.impl.SensorServiceImpl;
import org.openide.util.Cancellable;
import org.openide.util.RequestProcessor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jdesktop.swingx.mapviewer.GeoPosition;

import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.api.progress.ProgressHandleFactory;
import org.openide.util.Lookup;

/**
 *
 * @author I. de Vries
 */
public class LinkSensorDataToPlotTask implements Runnable, Cancellable {

    private static Log log = LogFactory.getLog(LinkSensorDataToPlotTask.class);
    private RequestProcessor.Task task;
    private ProgressHandle progressHandle;
    private boolean canceled = false;
    private SensorService sensorService = new SensorServiceImpl();
    private Map<SensorRunKey, SensorRunBO> sensorRunBOMap = new HashMap<SensorRunKey, SensorRunBO>();

    public void start() {
        task = RequestProcessor.getDefault().post(this);
        Cancellable myCancellable = task;
        progressHandle = ProgressHandleFactory.createHandle("Linking sensordata to plot", myCancellable);
        progressHandle.start(100);
    }

    @Override
    public void run() {
        linkSensorDataToPlot();
    }

    // Leave method public so it can be tested outside of a thread
    public void linkSensorDataToPlot() {
        try {
            sensorRunBOMap.clear();

            // Convert sensordata to jts point
            GeometryFactory geomFac = new GeometryFactory();
            List<SensDataHldr> points = new ArrayList<SensDataHldr>();
            List<GeoSensorDataBO> sensorDatas = sensorService.findAllGeoSensorDataNoSensorRun();// Slow progress -> 10 %
            if (progressHandle != null) {
                progressHandle.progress(10);
            }
            for (GeoSensorDataBO sensorData : sensorDatas) {
                SensDataHldr sdHldr = new SensDataHldr(geomFac.createPoint(new Coordinate(sensorData.getLongtitude(), sensorData.getLatitude())), sensorData);
                points.add(sdHldr);
            }
            // Convert sensordata to jts Polygon
            List<PlotHldr> plotPolys = new ArrayList<PlotHldr>();
            List<PlotBO> plots = sensorService.findAllPlotData();
            for (PlotBO plot : plots) {
                List<GeoPosition> geoPositions = wktToGeoPositions(plot.getWKT());
                Coordinate[] polygonCoordinates = new Coordinate[geoPositions.size() + 1];
                int i = 0;
                Coordinate cor1 = null;
                for (GeoPosition geoPosition : geoPositions) {
                    Coordinate cor = new Coordinate(geoPosition.getLongitude(), geoPosition.getLatitude());
                    if (i == 0) {
                        cor1 = cor;
                    }
                    polygonCoordinates[i] = cor;
                    i++;
                }
                // Set 1st point as last point to close the Liniar ring
                polygonCoordinates[i] = cor1;
                // Skip empty shapes
                if (polygonCoordinates.length > 1) {
                    LinearRing ring = geomFac.createLinearRing(polygonCoordinates);
                    Polygon polygon = geomFac.createPolygon(ring, null);
                    PlotHldr hldr = new PlotHldr(polygon, plot);
                    plotPolys.add(hldr);
                } else {
                    log.debug("Skipping empty plotShape");
                }
            }
            // Loop through all sensorData-Points to join data to Poly
            long totNrOfSensData = points.size();
            long pointsProcessed = 0;
            List<GeoSensorDataBO> persistQueue = new ArrayList<GeoSensorDataBO>();
            for (SensDataHldr sensData : points) {
                for (PlotHldr plot : plotPolys) {
                    if (sensData.pnt.intersects(plot.plg)) {
                        // We have a match, now persist this finding
                        SensorRunBO srBo = createSensorRunBO(sensData.bo.getGeoDataSensorFile(), plot.bo);
                        sensData.bo.setSensorRunBO(srBo);

                        persistQueue.add(sensData.bo);
                        // Commit every 250 updates
                        if ((persistQueue.size() % 250) == 0) {
                            sensorService.bulkPersist(persistQueue);
                            persistQueue.clear();
                        }
                        break;
                    }
                }
                pointsProcessed++;
                if (progressHandle != null) {
                    progressHandle.progress(10 + (int) (90 * ((double) pointsProcessed / (double) totNrOfSensData)));
                }
                // End the task in case it was canceled.
                if (canceled) {
                    break;
                }
            }
            // COmmit final uncomitted data
            sensorService.bulkPersist(persistQueue);

            for (LayerProvider provider : Lookup.getDefault().lookupAll(LayerProvider.class)) {
                if (provider instanceof SensorDataLayerProvider) {
                    for (Layer layer : ((SensorDataLayerProvider) provider).getLookup().lookupAll(Layer.class)) {
                        layer.updateLayer();
                    }
                }
            }


        } finally {
            if (progressHandle != null) {
                progressHandle.finish();
            }
            // Clear the local map
            sensorRunBOMap.clear();
        }
    }

    @Override
    public boolean cancel() {
        this.canceled = true;
        return this.canceled;
    }

    // TODO: duplicated form PlotFTO
    private List<GeoPosition> wktToGeoPositions(String wkt) {
        List<GeoPosition> geoPoly = new ArrayList<GeoPosition>();
        String breakdown = wkt.replaceAll("(MULTIPOLYGON|\\(|\\))", "");
        String[] pointArray = breakdown.split(",");
        for (String geoPos : pointArray) {
            String[] cor = geoPos.trim().split(" ");
            if (cor.length == 2) {
                double longt = Double.parseDouble(cor[0]);
                double lat = Double.parseDouble(cor[1]);
                geoPoly.add(new GeoPosition(lat, longt));
            }// else
        }
        return geoPoly;
    }

    private SensorRunBO createSensorRunBO(GeoSensorDataFileBO geoDataSensorFile, PlotBO bo) {
        SensorRunKey key = new SensorRunKey(geoDataSensorFile, bo);
        // Lookup the BO in out local cache
        SensorRunBO srBO = sensorRunBOMap.get(key);
        // Otherwise look it up in the database and put it tn the local cache
        if (srBO == null) {
            srBO = sensorService.findSensorRunBO(geoDataSensorFile, bo);
            sensorRunBOMap.put(key, srBO);
        }
        // Otherwise create a new one and put it tn the local cache
        if (srBO == null) {
            srBO = new SensorRunBO(geoDataSensorFile, bo);
            sensorService.persist(srBO);
            sensorRunBOMap.put(key, srBO);
        }
        return srBO;
    }

    private class SensorRunKey {

        private GeoSensorDataFileBO geoDataSensorFile;
        private PlotBO bo;

        public SensorRunKey(GeoSensorDataFileBO geoDataSensorFile, PlotBO bo) {
            this.geoDataSensorFile = geoDataSensorFile;
            this.bo = bo;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final SensorRunKey other = (SensorRunKey) obj;
            if (this.geoDataSensorFile != other.geoDataSensorFile && (this.geoDataSensorFile == null || !this.geoDataSensorFile.equals(other.geoDataSensorFile))) {
                return false;
            }
            if (this.bo != other.bo && (this.bo == null || !this.bo.equals(other.bo))) {
                return false;
            }
            return true;
        }

        @Override
        public int hashCode() {
            int hash = 7;
            return hash;
        }
    }

    // Met een MAP oplossen ?
    private class PlotHldr {

        Polygon plg;
        PlotBO bo;

        public PlotHldr(Polygon plg, PlotBO bo) {
            this.plg = plg;
            this.bo = bo;
        }
    }

    private class SensDataHldr {

        Point pnt;
        GeoSensorDataBO bo;

        public SensDataHldr(Point pnt, GeoSensorDataBO bo) {
            this.pnt = pnt;
            this.bo = bo;
        }
    }
}
